package sqlengine.queryOptimization;

import java.util.Map;
import java.util.ArrayList;
import java.util.HashMap;

import sqlengine.conditions.ConditionExecption;
import sqlengine.conditions.Conditions;
import sqlengine.database.DataFile;
import sqlengine.database.DataManager;

/*
 * generate optimized plan initial : Joined-table-Number-ArrayList with 0 0.
 * generate Joins according the element of diagonal 1. for each
 * condition-join on the non-diagonal condition matrix elements 2. estimate
 * the cost 3. insert into a priority Queue(PQ) 4. poll the least-cost join
 * from PQ 5. add the new temp table into tableNames-ArrayList, and
 * Joined-table-Number-ArrayList 6. set the lhsTableNo and rhsTableNo in the
 * Joined-table-Number-ArrayList with 1 7. add all the Joins that are
 * related with new temp table and old non-joined into PQ 8. poll out the
 * least-cost join which has not been joined from PQ 9. go to 5 if find a
 * join which has not been joined, else do CP with the tables that has not
 * been joined
 */

/*
 * estimate the cost initial : value-num-column-map<String TName.CName,int
 * value-num> with empty 0. according to the conditions, find value num in
 * the VMC-map 1. if can't find then call the value num counter function,
 * and add the num into VMC-map 2. estimate the cost according to the
 * formula 3. create a new column on new temp table, and make the num of
 * value of the two old columns refer to this new column 4. after the join
 * the the new column value num should be the smaller one 5. remove the
 * conditions that is used in the join 6. add entry in
 */

public class PlanOptimizer {
	// private ArrayList<Join> Plan;
	private Conditions conditions;

	// private ArrayList<String> tableNames;

	public PlanOptimizer(Conditions conditions) {
		this.conditions = conditions;
	}

	public String[] generateOptimizedPlan() throws ConditionExecption {
		// check the indexs on all the columns
		// use one conditions as join, the other as filers

		// one table, then return any one of ordinary conditions with index, if
		// there is
		// else return null

		if (conditions.getTableNum() == 1) {
			// open table
			DataFile dataFile = DataManager.getOpenedFiles().get(
					conditions.getTableNames()[0]);
			for (int i = 0; i < conditions.getConditions().get(0).size() / 2; i++) {
				if (dataFile.getIndex(conditions.getConditions().get(0).get(2 * i)) != null) {
					// if there is an index existing on some condition column
					// then delete the condition from the condition array
					// and return the condition
					String[] column = new String[2];
					column[0] = conditions.getConditions().get(0).get(2 * i);
					column[1] = conditions.getConditions().get(0)
							.get(2 * i + 1);
					conditions.getConditions().get(0).remove(2 * i);
					conditions.getConditions().get(0).remove(2 * i);

					return column;
				}
			}
			// if there is no index
			return null;
		} else {
			// two tables
			// 1. exist A.x = B.y, both x, y have indices
			// 2. exist A.x = "x", B.y = "y", both x, y have indices
			// 3. exist A.x = B.y, only x or y have indices
			// 4. exist A.x = "x" on one of the table

			DataFile firstDataFile = DataManager.getOpenedFiles().get(
					conditions.getTableNames()[0]);
			DataFile secondDataFile = DataManager.getOpenedFiles().get(
					conditions.getTableNames()[1]);

			int positionOfJoinCondition = 0;
			// 1. exist A.x = B.y, both x, y have indices
			ArrayList<String> joinCondition = conditions.getConditions().get(2);
			String[] condition = null;
			for (int i = 0; i < joinCondition.size() / 2; i++) {
				boolean indexNum[] = new boolean[2];
				indexNum[0] = false;
				indexNum[0] = false;
				if (firstDataFile.getIndex(joinCondition.get(2 * i)) != null ) {
					indexNum[0] = true;
				}
				if (secondDataFile.getIndex(joinCondition.get(2 * i + 1)) != null) {
					indexNum[1] = true;
				}
				if (indexNum[0] == true && indexNum[1] == true) {
					condition = new String[4];
					condition[0] = conditions.getTableNames()[0];
					condition[1] = joinCondition.get(2 * i);
					condition[2] = conditions.getTableNames()[1];
					condition[3] = joinCondition.get(2 * i + 1);
					joinCondition.remove(2 * i);
					joinCondition.remove(2 * i);
					return condition;
				} else if (indexNum[0] == true) {
					if (condition == null) {
						condition = new String[5];
						condition[0] = conditions.getTableNames()[0];
						condition[1] = joinCondition.get(2 * i);
						condition[2] = conditions.getTableNames()[1];
						condition[3] = joinCondition.get(2 * i + 1);
						condition[4] = "first";
						positionOfJoinCondition = 2 * i;
					}
				} else if (indexNum[1] == true) {
					if (condition == null) {
						condition = new String[5];
						condition[0] = conditions.getTableNames()[0];
						condition[1] = joinCondition.get(2 * i);
						condition[2] = conditions.getTableNames()[1];
						condition[3] = joinCondition.get(2 * i + 1);
						condition[4] = "second";
						positionOfJoinCondition = 2 * i;
					}
				}
			}

			// 2. exist A.x = "x", B.y = "y", both x, y have indices
			ArrayList<String> firstCondition = conditions.getConditions()
					.get(0);

			String firstNormalCondition[] = null;
			int positionOfFirstCondition = 0;
			for (int i = 0; i < firstCondition.size() / 2; i++) {
				if (firstDataFile.getIndex(firstCondition.get(2 * i)) != null) {
					firstNormalCondition = new String[3];
					firstNormalCondition[0] = conditions.getTableNames()[0];
					firstNormalCondition[1] = firstCondition.get(2 * i);
					firstNormalCondition[2] = firstCondition.get(2 * i + 1);
					positionOfFirstCondition = 2 * i;
					break;
				}
			}

			ArrayList<String> secondCondition = conditions.getConditions().get(
					1);

			String secondNormalCondition[] = null;
			int positionOfSecondCondition = 0;
			for (int i = 0; i < secondCondition.size() / 2; i++) {
				if (secondDataFile.getIndex(secondCondition.get(2 * i)) != null) {
					secondNormalCondition = new String[3];
					secondNormalCondition[0] = conditions.getTableNames()[1];
					secondNormalCondition[1] = secondCondition.get(2 * i);
					secondNormalCondition[2] = secondCondition.get(2 * i + 1);
					positionOfSecondCondition = 2 * i;
					break;
				}
			}
			if (firstNormalCondition != null && secondNormalCondition != null) {
				condition = new String[6];
				condition[0] = firstNormalCondition[0];
				condition[1] = firstNormalCondition[1];
				condition[2] = firstNormalCondition[2];
				condition[3] = secondNormalCondition[0];
				condition[4] = secondNormalCondition[1];
				condition[5] = secondNormalCondition[2];
				firstCondition.remove(positionOfFirstCondition);
				firstCondition.remove(positionOfFirstCondition);
				secondCondition.remove(positionOfSecondCondition);
				secondCondition.remove(positionOfSecondCondition);
				return condition;
			}
			// 3. exist A.x = B.y, only x or y have indices
			if (condition != null) {
				joinCondition.remove(positionOfJoinCondition);
				joinCondition.remove(positionOfJoinCondition);
				return condition;
			}
			// 4. exist A.x = "x" on one of the table
			if (firstNormalCondition != null) {
				firstCondition.remove(positionOfFirstCondition);
				firstCondition.remove(positionOfFirstCondition);
				return firstNormalCondition;
			}
			if (secondNormalCondition != null) {
				secondCondition.remove(positionOfSecondCondition);
				secondCondition.remove(positionOfSecondCondition);
				return secondNormalCondition;
			}

			return null;
		}
	}

	public void checkConsistency() throws QueryOptimizationException,
			ConditionExecption {

		// one table
		if (conditions.getTableNum() == 1) {
			Map<String, String> nonJoinConditionsMap = new HashMap<String, String>();
			ArrayList<String> condition = conditions.getConditions().get(0);
			for (int i = 0; i < condition.size() / 2; i++) {
				nonJoinConditionsMap.put(condition.get(2 * i),
						condition.get(2 * i + 1));
			}
			condition = conditions.getConditions().get(1);

			int lastMapSize;
			do {
				lastMapSize = nonJoinConditionsMap.size();

				for (int i = 0; i < condition.size() / 2; i++) {
					String lhsColumnName = condition.get(2 * i);
					String rhsColumnName = condition.get(2 * i + 1);
					if (nonJoinConditionsMap.containsKey(lhsColumnName)) {
						if (nonJoinConditionsMap.containsKey(rhsColumnName)) {
							if (nonJoinConditionsMap.get(lhsColumnName)
									.compareTo(
											nonJoinConditionsMap
													.get(rhsColumnName)) != 0) {
								throw new QueryOptimizationException(
										"conditions conflict");
							} else {
								condition.remove(2 * i);
								condition.remove(2 * i);
								i--;
							}
						} else {
							conditions.getConditions().get(0)
									.add(rhsColumnName);
							conditions
									.getConditions()
									.get(0)
									.add(nonJoinConditionsMap
											.get(lhsColumnName));
							condition.remove(2 * i);
							condition.remove(2 * i);
							i--;
						}
					} else {
						if (nonJoinConditionsMap.containsKey(rhsColumnName)) {
							conditions.getConditions().get(0)
									.add(lhsColumnName);
							conditions
									.getConditions()
									.get(0)
									.add(nonJoinConditionsMap
											.get(rhsColumnName));
							condition.remove(2 * i);
							condition.remove(2 * i);
							i--;
						} else {
							// do nothing
						}
					}
				}
			} while (lastMapSize != nonJoinConditionsMap.size());
		} else {
			// two table
			Map<String, String> nonJoinConditionsMap = new HashMap<String, String>();
			for (int i = 0; i < 2; i++) {
				ArrayList<String> conditionsArray = conditions.getConditions()
						.get(i);
				for (int j = 0; j < conditionsArray.size() / 2; j++) {
					// add "tableName.columnName , Value" pair into map
					nonJoinConditionsMap.put(conditions.getTableNames()[i]
							+ "." + conditionsArray.get(2 * j),
							conditionsArray.get(2 * j + 1));
				}
			}
			int lastMapSize;
			do {
				lastMapSize = nonJoinConditionsMap.size();

				for (int i = 2; i < 5; i++) {
					ArrayList<String> condition = conditions.getConditions()
							.get(i);
					for (int k = 0; k < condition.size() / 2; k++) {
						String lhs, rhs;
						if (i == 2) {// join
							lhs = conditions.getTableNames()[0] + "."
									+ condition.get(2 * k);
							rhs = conditions.getTableNames()[1] + "."
									+ condition.get(2 * k + 1);
						} else if (i == 3) {
							lhs = conditions.getTableNames()[0] + "."
									+ condition.get(2 * k);
							rhs = conditions.getTableNames()[0] + "."
									+ condition.get(2 * k + 1);
						} else {
							lhs = conditions.getTableNames()[1] + "."
									+ condition.get(2 * k);
							rhs = conditions.getTableNames()[1] + "."
									+ condition.get(2 * k + 1);
						}
						if (nonJoinConditionsMap.containsKey(lhs)) {
							if (nonJoinConditionsMap.containsKey(rhs)) {
								// both lhs and rhs are in map: check whether
								// conflict, and delete the condition
								if (nonJoinConditionsMap.get(lhs).compareTo(
										nonJoinConditionsMap.get(rhs)) != 0)
									throw new QueryOptimizationException(
											"exist non-consistent conditions");
								else {
									condition.remove(2 * k);
									condition.remove(2 * k);
									k--;
								}
							} else {
								// lhs is in map, rhs is not in map: delete the
								// condition
								// add the new condition into diagonal element
								String key, value;
								key = rhs;
								value = nonJoinConditionsMap.get(lhs);
								nonJoinConditionsMap.put(key, value);
								if (conditions.getTableNames()[0].compareTo(key
										.split("\\.")[0]) == 0) {
									// first table, put into 0
									conditions.getConditions().get(0)
											.add(key.split("\\.")[1]);
									conditions.getConditions().get(0)
											.add(value);
								} else {
									// second table, put into 1
									conditions.getConditions().get(1)
											.add(key.split("\\.")[1]);
									conditions.getConditions().get(1)
											.add(value);
								}
								nonJoinConditionsMap.put(rhs, value);

								condition.remove(2 * k);
								condition.remove(2 * k);
								k--;
							}
						} else {
							if (nonJoinConditionsMap.containsKey(rhs)) {
								// rhs is in map, lhs is not in map: delete the
								// condition
								// add the new condition into diagonal
								// elementString key, value;
								String key, value;
								key = lhs;
								value = nonJoinConditionsMap.get(rhs);
								nonJoinConditionsMap.put(key, value);
								if (conditions.getTableNames()[0].compareTo(key
										.split("\\.")[0]) == 0) {
									// first table, put into 0
									conditions.getConditions().get(0)
											.add(key.split("\\.")[1]);
									conditions.getConditions().get(0)
											.add(value);
								} else {
									// second table, put into 1
									conditions.getConditions().get(1)
											.add(key.split("\\.")[1]);
									conditions.getConditions().get(1)
											.add(value);
								}
								nonJoinConditionsMap.put(lhs, value);

								condition.remove(2 * k);
								condition.remove(2 * k);
								k--;

							} else {
								// both not exist in map, keep it there
							}
						}
					}
				}
			} while (lastMapSize != nonJoinConditionsMap.size());
		}
	}
}
